在学习 JavaScript 的时候,很多人都会遇到这样一个现象:

jsCopy

        
sum = 1 + 2;
console.log(sum); // 3
        
      

这段代码竟然可以正常运行,即使我们没有用 varletconst 去声明 sum

但是下面这段代码却会直接报错:

jsCopy

        
console.log(test); // ReferenceError: test is not defined
        
      

同样都是“没有声明”,为什么一个能用,一个会报错?
这背后其实涉及 JavaScript 中几个非常重要的概念:

  • 变量声明与赋值

  • 作用域查找

  • 隐式全局变量

  • 严格模式

  • ReferenceError 的触发机制

下面我们系统地讲清楚这个问题。


一、先看结论

先直接给出结论:

1. sum = 1 + 2 为什么能执行?

因为这不是“读取一个未定义变量”,而是给一个未声明的标识符赋值
非严格模式下,JavaScript 会帮你在全局对象上创建一个同名属性,相当于偷偷生成了一个全局变量。

jsCopy

        
sum = 1 + 2;

console.log(sum); // 3
console.log(window.sum); // 浏览器中也是 3
        
      

2. console.log(test) 为什么报错?

因为这里是在读取一个根本不存在的变量
JavaScript 在当前作用域和外层作用域一路查找,都找不到 test,于是抛出:

jsCopy

        
ReferenceError: test is not defined
        
      

二、赋值和读取,其实不是一回事

很多初学者会把这两种情况都理解为“变量没声明”,但 JavaScript 对它们的处理方式完全不同。

情况 1:对未声明变量进行赋值

jsCopy

        
sum = 1 + 2;
        
      

这里的动作是:赋值

在非严格模式下,JS 会认为你想在全局环境里创建一个变量,于是帮你做了这件事。

相当于:

jsCopy

        
window.sum = 3; // 浏览器环境
        
      

情况 2:直接读取未声明变量

jsCopy

        
console.log(test);
        
      

这里的动作是:读取

JS 引擎会去作用域链里查找 test

  1. 当前作用域有没有?

  2. 外层作用域有没有?

  3. 全局作用域有没有?

如果都没有,就说明这个标识符根本不存在,于是直接报错。


三、隐式全局变量:sum = 1 + 2 的本质

来看这段代码:

jsCopy

        
function foo() {
  sum = 1 + 2;
}

foo();
console.log(sum); // 3
        
      

虽然 sum 写在函数内部,但由于没有声明,在非严格模式下,sum 会变成一个隐式全局变量

这意味着:

  • 它不是函数私有的

  • 它会污染全局作用域

  • 项目稍微一大就容易引发命名冲突

例如:

jsCopy

        
function a() {
  total = 100;
}

function b() {
  total = 200;
}

a();
console.log(total); // 100

b();
console.log(total); // 200
        
      

这里的 total 就成了一个任何地方都能改的全局变量,非常危险。


四、为什么读取不存在的变量会报 ReferenceError

看这个例子:

jsCopy

        
console.log(test);
        
      

JS 在执行时需要先拿到 test 对应的值。
但如果这个变量根本没有被声明过,那么引擎连“这个名字对应哪块内存”都不知道,自然无法继续执行。

于是抛出:

jsCopy

        
ReferenceError: test is not defined
        
      

这里的关键点是:

  • 未声明:根本不存在这个变量

  • 未赋值:变量声明了,但值可能是 undefined

这两个概念千万不要混淆。


五、undefinednot defined 的区别

这是这个问题最容易延伸出的另一个知识点。

1. undefined:变量存在,但值是 undefined

jsCopy

        
var a;
console.log(a); // undefined
        
      

说明:

  • a 已经声明了

  • 只是还没赋值


2. not defined:变量根本没声明

jsCopy

        
console.log(b); // ReferenceError: b is not defined
        
      

说明:

  • b 根本不存在

  • 引擎找不到它


对比一下

jsCopy

        
var a;
console.log(a); // undefined

console.log(b); // ReferenceError: b is not defined
        
      

所以:

  • undefined 是一个值

  • not defined 是一种错误状态


六、严格模式下,sum = 1 + 2 也会报错

上面说的“直接赋值能用”,有一个前提:非严格模式

如果开启严格模式:

jsCopy

        
'use strict';

sum = 1 + 2; // ReferenceError: sum is not defined
        
      

为什么?

因为严格模式禁止创建隐式全局变量。
也就是说,JS 不再帮你“偷偷创建变量”,而是要求你必须明确声明。

这也是现代 JavaScript 推荐的写法。


七、浏览器里为什么会挂到 window 上?

在浏览器环境中,全局作用域和 window 对象有密切关系。

比如:

jsCopy

        
var a = 1;
console.log(window.a); // 1
        
      

而对于非严格模式下的隐式全局变量:

jsCopy

        
b = 2;
console.log(window.b); // 2
        
      

所以很多时候你会看到:

  • var 声明的全局变量,会成为 window 的属性

  • 未声明直接赋值生成的隐式全局变量,也会成为 window 的属性

但是要注意,letconst 不一样:

jsCopy

        
let c = 3;
const d = 4;

console.log(window.c); // undefined
console.log(window.d); // undefined
        
      

它们虽然处于全局作用域,但不会成为 window 的属性


八、从执行机制上理解这个现象

如果从更底层一点的角度理解,可以把 JS 的处理过程分成两类:

1. 读取标识符

比如:

jsCopy

        
console.log(test);
        
      

引擎会做变量解析:

  • 查找 test

  • 找不到

  • ReferenceError


2. 给标识符赋值

比如:

jsCopy

        
sum = 3;
        
      

引擎会先尝试查找 sum 对应的引用。

  • 如果找到了,就赋值

  • 如果没找到:

    • 非严格模式:创建一个全局属性

    • 严格模式:抛 ReferenceError

所以“读取”和“赋值”虽然都涉及变量名,但底层行为并不一样。


九、实际开发中为什么要避免这种写法

虽然下面这种代码能跑:

jsCopy

        
sum = 1 + 2;
        
      

但在工程实践里,这是非常不推荐的写法,原因包括:

1. 污染全局作用域

变量会泄漏到全局,影响整个应用。

2. 容易产生命名冲突

多个模块可能不小心使用同名变量。

3. 难以排查 Bug

你以为是局部变量,实际上它跑到了全局。

4. 严格模式下直接报错

现代前端工程中,大多数代码都会默认运行在更严格的环境中。

5. 不利于代码维护

团队协作时,隐式行为会让代码意图不清晰。


十、正确写法是什么?

始终使用 letconstvar 明确声明变量。

推荐写法

jsCopy

        
const sum = 1 + 2;
console.log(sum);
        
      

如果变量后续会变化:

jsCopy

        
let total = 0;
total = total + 10;
        
      

除非是兼容旧代码,否则现在一般不推荐优先使用 var


十一、面试里可以怎么回答这个问题

如果面试官问:

为什么 sum = 1 + 2 可以执行,而 console.log(test) 会报错?

你可以这样回答:

因为两者本质上不是同一种操作。
sum = 1 + 2 是给一个未声明变量赋值,在非严格模式下,JavaScript 会自动把它创建为一个隐式全局变量;
console.log(test) 是读取一个未声明的变量,JS 引擎在作用域链中找不到这个标识符时,就会抛出 ReferenceError
如果开启严格模式,那么对未声明变量赋值也会报错。

这个回答就比较完整了。


十二、总结

最后再总结一遍:

sum = 1 + 2 能运行的原因

  • 它是“赋值操作”

  • 在非严格模式下

  • JS 会创建隐式全局变量

console.log(test) 报错的原因

  • 它是“读取操作”

  • test 没有被声明

  • 作用域链中找不到

  • 所以抛出 ReferenceError

最佳实践

  • 永远使用 let / const 声明变量

  • 避免隐式全局变量

  • 使用严格模式或现代工程化工具避免这类问题


十三、一个完整示例

jsCopy

        
// 非严格模式
function demo() {
  sum = 1 + 2; // 隐式全局变量
  console.log(sum); // 3
}

demo();

console.log(sum); // 3
console.log(window.sum); // 浏览器中为 3

console.log(test); // ReferenceError: test is not defined
        
      

严格模式下:

jsCopy

        
'use strict';

function demo() {
  sum = 1 + 2; // ReferenceError: sum is not defined
}

demo();